/*
 * Copyright (C) 2011-2012 Wojciech Dzierżanowski
 * See LICENSE.txt for licensing details.
 */

package wdzierzan.downstream.android.servermanager;

import android.content.Context;
import android.content.DialogInterface.OnCancelListener;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import wdzierzan.downstream.android.Format;
import wdzierzan.downstream.android.R;
import wdzierzan.downstream.core.Streamer;

/**
 * @author Wojciech Dzierżanowski (wojciech.dzierzanowski@gmail.com)
 */
public class ServerManager implements SharedPreferences.OnSharedPreferenceChangeListener {

    private class ServerImpl implements Server {
        public int id;
        public String name;
        public String hostname;
        public String username; 
        public String musicPath;

        public int getId() {
            return id;
        }
        public String getName() {
            return name;
        }
        public String getHostname() {
            return hostname;
        }
        public String getUsername() {
            return username;
        }
        public String getMusicPath() {
            return musicPath;
        }

        @Override
        public String toString() {
            return new StringBuilder("ServerImpl{").append("id=").append(id)
                    .append(", name=").append(name)
                    .append(", hostname=").append(hostname)
                    .append(", username=").append(username)
                    .append(", musicPath=").append(musicPath).append('}').toString();
        }
    }

    private static final Logger logger = Logger.getLogger(ServerManager.class.getName());

    private static final String PREFERENCE_LAST_ID = "last-id";
    private static final String PREFERENCE_ACTIVE_ID = "active-id";
    public static final String PREFERENCE_FORMAT = "format";

    public static final int STREAM_PORT = 3000;

    private static ServerManager instance;

    private Context appContext;
    private SharedPreferences prefs;
    private StreamerHolder activeStreamerHolder;
    private Server[] cachedServers;
    private boolean dirty = true;

    /** Maps server ID to {@code StreamerHolder}. */
    private Map<Integer, StreamerHolder> streamerHolders = new HashMap<Integer, StreamerHolder>();

    private ServerManager() {
    }

    public static synchronized ServerManager getInstance() {
        if (instance == null)
            instance = new ServerManager();
        return instance;
    }

    public void initialize(Context context) {
        if (prefs != null)
            prefs.unregisterOnSharedPreferenceChangeListener(this);

        appContext = context.getApplicationContext();
        prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
        prefs.registerOnSharedPreferenceChangeListener(this);
    }

    public void dispose() {
        prefs.unregisterOnSharedPreferenceChangeListener(this);
    }


    public Server getActiveServer() {
        int index = getActiveServerIndex();
        return index >= 0 ? getAllServers()[index] : null;
    }

    /**
     * @return index of active server.  This is an index into the array returned
     *      from getAllServers().  If no server is active, @code -1 is returned.
     */
    public int getActiveServerIndex() {
        int activeId = getActiveServerId();
        if (activeId >= 0) {
            Server[] servers = getAllServers();
            for (int i = 0; i < servers.length; i++)
                if (servers[i].getId() == activeId) {
                    logger.info("Server no. " + i + ", ID=" + getActiveServerId() + " is the active one");
                    return i;
                }
        }
        logger.info("No active server");
        return -1;
    }

    public void setActiveServerIndex(int active) {
        if (active < 0 || active >= getAllServers().length)
            throw new IndexOutOfBoundsException();
        int activeId = getAllServers()[active].getId();
        setActiveServerId(activeId);
    }

    private int getActiveServerId() {
        return prefs.getInt(PREFERENCE_ACTIVE_ID, -1);
    }

    private void setActiveServerId(int activeId) {
        if (activeId == getActiveServerId())
            return;

        logger.info("New active server ID=" + activeId);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putInt(PREFERENCE_ACTIVE_ID, activeId);
        if (editor.commit())
            activeStreamerHolder = null;
    }

    /**
     * @return ID of new server
     */
    public int createServer() {
        int lastId = prefs.getInt(PREFERENCE_LAST_ID, -1);
        logger.info("Last used ID: " + lastId);
        for (int id = 0; id <= lastId + 1; id++)
            if (readServer(id) == null) {

                if (id > lastId) {
                    SharedPreferences.Editor editor = prefs.edit();
                    editor.putInt(PREFERENCE_LAST_ID, id);
                    if (!editor.commit())
                        break;
                }
                logger.info("New server created, ID = " + id);

                // If it was the first server that was created, set it as the
                // active one.
                if (id == 0)
                    setActiveServerId(0);

                return id;
            }
        assert false : "Your prefs are a mess";
        return 0;
    }

    /**
     * @param index index of server to remove.  This is an index into the array
     *      returned from getAllServers().
     */
    public void removeServer(int index) {
        removeServerById(getAllServers()[index].getId());
    }

    public Server[] getAllServers() {
        if (dirty) {
            cachedServers = readServers();
            dirty = false;
        }
        return cachedServers;
    }

    
    /**
     * Returns a {@code Streamer} object for the active server, provided one
     * has been initialized with {@code initializeStreamer()}.
     *
     * The caller must pair <em>every</em> non-{@code null}-returning call to
     * {@code acquireStreamer()} with a subsequent call to
     * {@code releaseStreamer()} to guarantee proper resource clean-up.
     *
     * @return a {@code Streamer} object or {@code null} if a call to
     *      {@code initializeStreamer()} is required first
     * @see #initializeStreamer
     * @see #releaseStreamer
     */
    public Streamer acquireStreamer() {
        if (getActiveServerId() < 0)
            throw new IllegalStateException("Need active server");

        if (activeStreamerHolder == null)
            activeStreamerHolder = streamerHolders.get(getActiveServerId());
        if (activeStreamerHolder == null)
            activeStreamerHolder = new StreamerHolder();

        Streamer streamer = activeStreamerHolder.getStreamer();
        if (streamer != null)
            streamerHolders.put(getActiveServerId(), activeStreamerHolder);

        return streamer;
    }

    public void releaseStreamer(Streamer streamer) {
        StreamerHolder unreferencedHolder = null;
        for (StreamerHolder streamerHolder : streamerHolders.values()) {
            streamerHolder.releaseStreamer(streamer);
            if (!streamerHolder.hasReferences()) {
                if (unreferencedHolder != null)
                    throw new IllegalStateException("Only one can go unreferenced at a time");
                unreferencedHolder = streamerHolder;
            }
        }
        if (unreferencedHolder != null)
            streamerHolders.values().remove(unreferencedHolder);
    }

    public void initializeStreamer(Context activityContext,
            Runnable onSuccess, Runnable onFailure, OnCancelListener onCancel) {
        Server activeServer = getActiveServer();
        if (activeServer == null || activeStreamerHolder == null)
            throw new IllegalStateException("Need active server");

        activeStreamerHolder.initializeStreamer(activeServer.getHostname(), STREAM_PORT,
                activeServer.getUsername(), getFormat(), activityContext, onSuccess, onFailure, onCancel);
    }


    private Server[] readServers() {
        ArrayList<Server> serverList = new ArrayList<Server>();

        int lastId = prefs.getInt(PREFERENCE_LAST_ID, -1);
        for (int id = 0; id <= lastId; id++) {
            Server server = readServer(id);
            if (server != null)
                serverList.add(server);
        }

        return serverList.toArray(new Server[]{});
    }

    private Server readServer(int serverId) {
        ServerImpl server = new ServerImpl();
        server.name = getPreference(R.string.server_name, serverId);
        if (server.name == null) {
            logger.info("No server with ID " + serverId);
            return null;
        }
        server.hostname = getPreference(R.string.server_hostname, serverId);
        server.username = getPreference(R.string.server_username, serverId);
        server.musicPath = getPreference(R.string.server_dir_path, serverId);
        server.id = serverId;
        logger.info("Read server " + server);
        return server;
    }

    private void removeServerById(int serverId) {
        SharedPreferences.Editor editor = prefs.edit();

        editor.remove(getPreferenceKey(R.string.server_name, serverId));
        editor.remove(getPreferenceKey(R.string.server_hostname, serverId));
        editor.remove(getPreferenceKey(R.string.server_username, serverId));
        editor.remove(getPreferenceKey(R.string.server_dir_path, serverId));

        int lastId = prefs.getInt(PREFERENCE_LAST_ID, -1);
        if (serverId == prefs.getInt(PREFERENCE_LAST_ID, -1)) {
            lastId = serverId - 1;
            for (; lastId >= 0; lastId--)
                if (readServer(lastId) != null)
                    break;
            editor.putInt(PREFERENCE_LAST_ID, lastId);
        }

        if (serverId == getActiveServerId()) {
            activeStreamerHolder = null;
            if (getAllServers().length == 2)
                editor.putInt(PREFERENCE_ACTIVE_ID, lastId);
            else
                editor.putInt(PREFERENCE_ACTIVE_ID, -1);
        }

        StreamerHolder streamerHolder = streamerHolders.remove(serverId);
        if (streamerHolder != null)
            streamerHolder.disconnectStreamer();

        boolean result = editor.commit();
        logger.info("Removing server with ID " + serverId + ": "
                + (result ? "success" : "failure"));
    }

    private String getPreference(int keyId, int serverId) {
        return prefs.getString(getPreferenceKey(keyId, serverId), null);
    }

    private String getPreferenceKey(int keyId, int serverId) {
        return getPreferenceKey(appContext.getString(keyId), serverId);
    }

    public String getPreferenceKey(String key, int serverId) {
        return "" + serverId + ':' + key;
    }

    public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
        if (key.equals(PREFERENCE_LAST_ID)) {
            dirty = true;
        } else if (key.equals(PREFERENCE_FORMAT)) {
            // Disconnect all the streamers so that they pick up the new format
            // setting.
            for (StreamerHolder streamerHolder : streamerHolders.values())
                streamerHolder.disconnectStreamer();
        } else {
            int pos = key.indexOf(':');
            if (pos != -1) {
                String suffix = key.substring(pos + 1);
                if (suffix.equals(appContext.getString(R.string.server_name)) || suffix.equals(appContext.getString(R.string.server_dir_path))) {
                    dirty = true;
                } else if (suffix.equals(appContext.getString(R.string.server_hostname)) || suffix.equals(appContext.getString(R.string.server_username))) {
                    dirty = true;

                    String prefix = key.substring(0, pos);
                    try {
                        int serverId = Integer.parseInt(prefix);
                        if (serverId >= 0) {
                            StreamerHolder streamerHolder = streamerHolders.get(serverId);
                            if (streamerHolder != null)
                                streamerHolder.disconnectStreamer();
                        }
                    } catch (NumberFormatException e) {
                        // do nothing
                    }
                }
            }
        }

        logger.info("Prefs changed, we're " + (dirty ? "dirty" : "still clean"));
    }

    public Format getFormat() {
        return Format.valueOf(prefs.getString(PREFERENCE_FORMAT, Format.OGG_VORBIS.name()));
    }
}
